Опануйте FastAPI middleware з нуля. Цей поглиблений посібник охоплює власні middleware, аутентифікацію, ведення журналу, обробку помилок і найкращі практики для створення надійних API.
Python FastAPI Middleware: Повний посібник з обробки запитів і відповідей
У світі сучасної веб-розробки продуктивність, безпека та зручність обслуговування мають першорядне значення. Фреймворк Python FastAPI швидко набув популярності завдяки своїй неймовірній швидкості та зручним для розробників функціям. Однією з його найпотужніших, але іноді незрозумілих функцій є middleware. Middleware діє як вирішальна ланка в ланцюжку обробки запитів і відповідей, дозволяючи розробникам виконувати код, змінювати дані та застосовувати правила до того, як запит досягне свого призначення, або до того, як відповідь буде надіслана клієнту.
Цей вичерпний посібник розроблено для глобальної аудиторії розробників, від тих, хто тільки починає працювати з FastAPI, до досвідчених професіоналів, які прагнуть поглибити своє розуміння. Ми розглянемо основні концепції middleware, продемонструємо, як створювати власні рішення, і розглянемо практичні, реальні випадки використання. Зрештою, ви будете готові використовувати middleware для створення більш надійних, безпечних і ефективних API.
Що таке Middleware в контексті веб-фреймворків?
Перш ніж занурюватися в код, важливо зрозуміти концепцію. Уявіть цикл запиту-відповіді вашої програми як конвеєр або складальну лінію. Коли клієнт надсилає запит до вашого API, він не просто миттєво потрапляє в логіку вашої кінцевої точки. Замість цього він проходить через серію етапів обробки. Подібним чином, коли ваша кінцева точка генерує відповідь, вона повертається назад через ці етапи, перш ніж досягти клієнта. Компоненти Middleware є саме цими етапами в конвеєрі.
Популярною аналогією є цибульна модель. Ядром цибулі є бізнес-логіка вашої програми (кінцева точка). Кожен шар цибулі, що оточує ядро, є частиною middleware. Запит повинен пройти через кожен зовнішній шар, щоб дістатися до ядра, а відповідь повертається назад через ті самі шари. Кожен шар може перевіряти та змінювати запит на шляху до ядра та відповідь на шляху з нього.
По суті, middleware - це функція або клас, який має доступ до об’єкта запиту, об’єкта відповіді та наступного middleware в циклі запиту-відповіді програми. Його основні цілі включають:
- Виконання коду: Виконання дій для кожного вхідного запиту, таких як ведення журналу або моніторинг продуктивності.
- Зміна запиту та відповіді: Додавання заголовків, стиснення тіл відповідей або перетворення форматів даних.
- Скорочення циклу: Завершення циклу запиту-відповіді достроково. Наприклад, middleware аутентифікації може заблокувати неаутентифікований запит до того, як він досягне призначеної кінцевої точки.
- Управління глобальними проблемами: Обробка наскрізних проблем, таких як обробка помилок, CORS (спільне використання ресурсів між різними джерелами) і керування сеансами в централізованому місці.
FastAPI побудовано на основі інструментарію Starlette, який забезпечує надійну реалізацію стандарту ASGI (Asynchronous Server Gateway Interface). Middleware є фундаментальною концепцією в ASGI, що робить його першокласним елементом в екосистемі FastAPI.
Найпростіша форма: FastAPI Middleware з декоратором
FastAPI надає простий спосіб додати middleware за допомогою декоратора @app.middleware("http"). Це ідеально підходить для простої, самодостатньої логіки, яку потрібно запускати для кожного HTTP-запиту.
Створімо класичний приклад: middleware для обчислення часу обробки для кожного запиту та додавання його до заголовків відповіді. Це надзвичайно корисно для моніторингу продуктивності.
Приклад: Middleware часу обробки
Спочатку переконайтеся, що у вас встановлено FastAPI та ASGI-сервер, наприклад Uvicorn:
pip install fastapi uvicorn
Тепер напишемо код у файлі з назвою main.py:
import time
from fastapi import FastAPI, Request
app = FastAPI()
# Визначення функції middleware
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# Запис часу початку, коли надходить запит
start_time = time.time()
# Перехід до наступного middleware або кінцевої точки
response = await call_next(request)
# Обчислення часу обробки
process_time = time.time() - start_time
# Додавання власного заголовка до відповіді
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get("/")
async def root():
# Імітація деякої роботи
time.sleep(0.5)
return {"message": "Hello, World!"}
Щоб запустити цю програму, використовуйте команду:
uvicorn main:app --reload
Тепер, якщо ви надішлете запит до http://127.0.0.1:8000 за допомогою інструменту, як-от cURL, або API-клієнта, як-от Postman, ви побачите новий заголовок у відповіді, X-Process-Time, зі значенням приблизно 0,5 секунди.
Розбір коду:
@app.middleware("http"): Цей декоратор реєструє нашу функцію як частину HTTP middleware.async def add_process_time_header(request: Request, call_next):: Функція middleware має бути асинхронною. Вона отримує вхідний об’єктRequestі спеціальну функціюcall_next.response = await call_next(request): Це найважливіший рядок.call_nextпередає запит на наступний крок конвеєра (іншому middleware або фактичній операції шляху). Ви повинні `await` цей виклик. Результатом є об’єктResponse, згенерований кінцевою точкою.response.headers[...] = ...: Після отримання відповіді від кінцевої точки ми можемо змінити її, у цьому випадку, додавши власний заголовок.return response: Нарешті, змінена відповідь повертається для надсилання клієнту.
Створення власних Middleware класів
Хоча підхід з використанням декораторів є простим, він може стати обмежуючим для складніших сценаріїв, особливо коли ваш middleware потребує налаштування або потребує керування деяким внутрішнім станом. У цих випадках FastAPI (через Starlette) підтримує middleware на основі класів за допомогою BaseHTTPMiddleware.
Підхід на основі класів пропонує кращу структуру, дозволяє вводити залежності в його конструктор і, як правило, більш зручний в обслуговуванні для складної логіки. Основна логіка знаходиться в асинхронному методі dispatch.
Приклад: Middleware аутентифікації API-ключем на основі класу
Створімо більш практичний middleware, який захищає наш API. Він перевірятиме наявність певного заголовка, X-API-Key, і якщо ключ відсутній або недійсний, він негайно поверне відповідь про помилку 403 Forbidden. Це приклад «скорочення» запиту.
У main.py:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import Response
# Список дійсних API-ключів. У реальній програмі це надходило б з бази даних або захищеного сховища.
VALID_API_KEYS = ["my-super-secret-key", "another-valid-key"]
class APIKeyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
api_key = request.headers.get("X-API-Key")
if api_key not in VALID_API_KEYS:
# Скорочення запиту та повернення відповіді про помилку
return JSONResponse(
status_code=403,
content={"detail": "Forbidden: Invalid or missing API Key"}
)
# Якщо ключ дійсний, продовження з запитом
response = await call_next(request)
return response
app = FastAPI()
# Додавання middleware до програми
app.add_middleware(APIKeyMiddleware)
@app.get("/")
async def root():
return {"message": "Welcome to the secure zone!"}
Тепер, коли ви запускаєте цю програму:
- Запит без заголовка
X-API-Key(або з неправильним значенням) отримає код стану 403 і повідомлення про помилку JSON. - Запит з заголовком
X-API-Key: my-super-secret-keyбуде успішним і отримає відповідь 200 OK.
Цей шаблон надзвичайно потужний. Коду кінцевої точки в / не потрібно нічого знати про перевірку API-ключа; ця проблема повністю відокремлена в шар middleware.
Поширені та потужні випадки використання Middleware
Middleware - ідеальний інструмент для обробки наскрізних проблем. Розгляньмо деякі з найпоширеніших і найефективніших випадків використання.
1. Централізоване ведення журналу
Комплексне ведення журналу є обов’язковим для виробничих програм. Middleware дозволяє створити єдину точку, де ви реєструєте важливу інформацію про кожен запит і відповідну відповідь.
Приклад Logging Middleware:
import logging
from fastapi import FastAPI, Request
import time
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
start_time = time.time()
# Реєстрація деталей запиту
logger.info(f"Вхідний запит: {request.method} {request.url.path}")
response = await call_next(request)
process_time = time.time() - start_time
# Реєстрація деталей відповіді
logger.info(f"Статус відповіді: {response.status_code} | Час обробки: {process_time:.4f}s")
return response
Це middleware реєструє метод і шлях запиту на вході та код стану відповіді та загальний час обробки на виході. Це забезпечує безцінну видимість трафіку вашої програми.
2. Глобальна обробка помилок
За замовчуванням необроблений виняток у вашому коді призведе до помилки 500 Internal Server Error, потенційно відкриваючи трасування стека та деталі реалізації клієнту. Глобальне middleware обробки помилок може перехоплювати всі винятки, реєструвати їх для внутрішнього перегляду та повертати стандартизовану, зручну для користувача відповідь про помилку.
Приклад Middleware обробки помилок:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def error_handling_middleware(request: Request, call_next):
try:
return await call_next(request)
except Exception as e:
logger.error(f"Виникла необроблена помилка: {e}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "Виникла внутрішня помилка сервера. Спробуйте ще раз пізніше."}
)
@app.get("/error")
async def cause_error():
return 1 / 0 # Це викличе ZeroDivisionError
Завдяки цьому middleware запит до /error більше не призведе до збою сервера або розкриття трасування стека. Замість цього він чемно поверне код стану 500 з чистим тілом JSON, тоді як повна помилка буде зареєстрована на стороні сервера для дослідження розробниками.
3. CORS (спільне використання ресурсів між різними джерелами)
Якщо ваша зовнішня програма обслуговується з іншого домену, протоколу або порту, ніж ваш внутрішній сервер FastAPI, браузери блокуватимуть запити через політику одного походження. CORS — це механізм для пом’якшення цієї політики. FastAPI надає спеціальний, добре налаштований `CORSMiddleware` саме для цієї мети.
Приклад конфігурації CORS:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Визначення списку дозволених джерел. Використовуйте "*" для загальнодоступних API, але будьте конкретними для кращої безпеки.
origins = [
"http://localhost:3000",
"https://my-production-frontend.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # Дозвіл на включення файлів cookie в міждоменні запити
allow_methods=["*"], # Дозвіл на всі стандартні HTTP-методи
allow_headers=["*"], # Дозвіл на всі заголовки
)
Це один із перших middleware, які ви, ймовірно, додасте до будь-якого проекту з відокремленою зовнішньою частиною, що спрощує керування політиками між джерелами з одного центрального розташування.
4. Стиснення GZip
Стиснення відповідей HTTP може значно зменшити їх розмір, що призведе до швидшого завантаження для клієнтів і зменшення витрат на пропускну здатність. FastAPI включає `GZipMiddleware` для автоматичної обробки цього.
Приклад GZip Middleware:
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
# Додавання GZip middleware. Ви можете встановити мінімальний розмір для стиснення.
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/")
async def root():
# Ця відповідь невелика і не буде заархівована.
return {"message": "Hello World"}
@app.get("/large-data")
async def large_data():
# Ця велика відповідь буде автоматично заархівована middleware.
return {"data": "a_very_long_string..." * 1000}
Завдяки цьому middleware будь-яка відповідь, розмір якої перевищує 1000 байт, буде стиснута, якщо клієнт вказує, що він приймає кодування GZip (що роблять практично всі сучасні браузери та клієнти).
Розширені концепції та найкращі практики
Оскільки ви стаєте більш досвідченими з middleware, важливо розуміти деякі нюанси та найкращі практики для написання чистого, ефективного та передбачуваного коду.
1. Порядок Middleware має значення!
Це найважливіше правило, яке слід пам’ятати. Middleware обробляється в порядку його додавання до програми. Перший доданий middleware є найзовнішнім шаром «цибулі».
Розгляньмо таку конфігурацію:
app.add_middleware(ErrorHandlingMiddleware) # Найовнішніший
app.add_middleware(LoggingMiddleware)
app.add_middleware(AuthenticationMiddleware) # Найвнутрішніший
Потік запиту буде таким:
ErrorHandlingMiddlewareотримує запит. Він обертає свій `call_next` у блок `try...except`.- Він викликає `next`, передаючи запит до `LoggingMiddleware`.
LoggingMiddlewareотримує запит, реєструє його та викликає `next`.AuthenticationMiddlewareотримує запит, перевіряє облікові дані та викликає `next`.- Запит нарешті досягає кінцевої точки.
- Кінцева точка повертає відповідь.
AuthenticationMiddlewareотримує відповідь і передає її вище.LoggingMiddlewareотримує відповідь, реєструє її та передає її вище.ErrorHandlingMiddlewareотримує остаточну відповідь і повертає її клієнту.
Цей порядок є логічним: обробник помилок знаходиться зовні, щоб він міг перехоплювати помилки з будь-якого наступного шару, включаючи інший middleware. Шар аутентифікації знаходиться глибоко всередині, тому ми не турбуємося про реєстрацію або обробку запитів, які все одно будуть відхилені.
2. Передача даних за допомогою `request.state`
Іноді middleware потрібно передати інформацію в кінцеву точку. Наприклад, middleware аутентифікації може декодувати JWT і витягувати ідентифікатор користувача. Як він може зробити цей ідентифікатор користувача доступним для функції операції шляху?
Неправильний спосіб — безпосередньо змінювати об’єкт запиту. Правильний спосіб — використовувати об’єкт request.state. Це простий, порожній об’єкт, наданий саме для цієї мети.
Приклад: Передача даних користувача з Middleware
# У методі dispatch вашого middleware аутентифікації:
# ... після перевірки маркера та декодування користувача ...
user_data = {"id": 123, "username": "global_dev"}
request.state.user = user_data
response = await call_next(request)
# У вашій кінцевій точці:
@app.get("/profile")
async def get_user_profile(request: Request):
current_user = request.state.user
return {"profile_for": current_user}
Це зберігає логіку чистою та дозволяє уникнути забруднення простору імен об’єкта `Request`.
3. Міркування щодо продуктивності
Хоча middleware є потужним, кожен шар додає невелику кількість накладних витрат. Для високопродуктивних програм враховуйте такі моменти:
- Зберігайте його лаконічним: Логіка Middleware має бути якомога швидшою та ефективнішою.
- Будьте асинхронними: Якщо вашому middleware потрібно виконувати операції вводу-виводу (наприклад, перевірку бази даних), переконайтеся, що він повністю `async`, щоб уникнути блокування циклу подій сервера.
- Використовуйте з метою: Не додавайте middleware, який вам не потрібен. Кожен з них збільшує глибину стеку викликів і час обробки.
4. Тестування вашого Middleware
Middleware є критичною частиною логіки вашої програми, і його слід ретельно перевіряти. `TestClient` FastAPI робить це простим. Ви можете писати тести, які надсилають запити з необхідними умовами (наприклад, з дійсним API-ключем і без нього) і стверджувати, що middleware поводиться належним чином.
Приклад тесту для APIKeyMiddleware:
from fastapi.testclient import TestClient
from .main import app # Імпортуйте свою програму FastAPI
client = TestClient(app)
def test_request_without_api_key_is_forbidden():
response = client.get("/")
assert response.status_code == 403
assert response.json() == {"detail": "Forbidden: Invalid or missing API Key"}
def test_request_with_valid_api_key_is_successful():
headers = {"X-API-Key": "my-super-secret-key"}
response = client.get("/", headers=headers)
assert response.status_code == 200
assert response.json() == {"message": "Welcome to the secure zone!"}
Висновок
FastAPI middleware - це фундаментальний і потужний інструмент для будь-якого розробника, який створює сучасні веб-API. Він забезпечує елегантний і багаторазовий спосіб обробки наскрізних проблем, відокремлюючи їх від основної бізнес-логіки. Перехоплюючи та обробляючи кожен запит і відповідь, middleware дозволяє реалізувати надійне ведення журналу, централізовану обробку помилок, сувору політику безпеки та покращення продуктивності, як-от стиснення.
Від простого декоратора @app.middleware("http") до складних рішень на основі класів, у вас є можливість вибрати правильний підхід для ваших потреб. Розуміючи основні концепції, поширені випадки використання та найкращі практики, такі як упорядкування middleware та керування станом, ви можете створювати чистіші, безпечніші та зручніші в обслуговуванні програми FastAPI.
Тепер ваша черга. Почніть інтегрувати власний middleware у свій наступний проект FastAPI та відкрийте новий рівень контролю та елегантності в дизайні вашого API. Можливості величезні, і освоєння цієї функції, безсумнівно, зробить вас більш ефективним і продуктивним розробником.